package org.loong.crypto.core.utils;

import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECPoint;
import java.security.spec.ECPrivateKeySpec;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.gm.GMObjectIdentifiers;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jcajce.provider.asymmetric.util.EC5Util;
import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil;
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.math.ec.FixedPointCombMultiplier;
import org.bouncycastle.util.BigIntegers;
import org.loong.crypto.common.exception.CryptoException;
import org.loong.crypto.common.exception.InvalidKeyException;
import org.loong.crypto.core.GlobalBouncyCastleProvider;

/**
 * EC Key conversion utility.
 */
public class ECKeyUtils {

    /**
     * SM2默认曲线.
     */
    public static final String SM2_CURVE_NAME = "sm2p256v1";

    /**
     * Returns the public EC key of the specified EC key.
     *
     * @param key the EC key bytes. Must not be {@code null}.
     * @return the EC public key.
     * @throws CryptoException the EC key transformation failed.
     */
    public static ECPublicKey toECPublicKey(final byte[] key) throws CryptoException {
        final Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider();
        try {
            X509EncodedKeySpec eks = new X509EncodedKeySpec(key);
            KeyFactory keyFactory = KeyFactory.getInstance("EC", provider);
            ECPublicKey publicKey = (BCECPublicKey) keyFactory.generatePublic(eks);
            if (publicKey == null) {
                throw new InvalidKeyException("The EC public key transformation failed");
            }
            return publicKey;
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            throw new InvalidKeyException("The EC public key transformation failed");
        }
    }

    /**
     * Returns the public EC key of the specified EC key.
     *
     * @param x the EC key x bytes. Must not be {@code null}.
     * @param y the EC key y bytes. Must not be {@code null}.
     * @return the EC public key.
     * @throws CryptoException the EC key transformation failed.
     */
    public static ECPublicKey convertECPublicKey(final byte[] x, final byte[] y) throws CryptoException {
        final Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider();
        try {
            X9ECParameters x9ECParameters = GMNamedCurves.getByName(SM2_CURVE_NAME);
            ECCurve curve = x9ECParameters.getCurve();
            ECPoint point = EC5Util.convertPoint(curve.createPoint(BigIntegers.fromUnsignedByteArray(x), BigIntegers.fromUnsignedByteArray(y)));
            ECNamedCurveSpec ecSpec = new ECNamedCurveSpec(SM2_CURVE_NAME, curve, x9ECParameters.getG(), x9ECParameters.getN());

            KeyFactory keyFactory = KeyFactory.getInstance("EC", provider);
            ECPublicKey publicKey = (BCECPublicKey) keyFactory.generatePublic(new ECPublicKeySpec(point, ecSpec));
            if (publicKey == null) {
                throw new InvalidKeyException("The EC public key transformation failed");
            }
            return publicKey;
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            throw new InvalidKeyException("The EC public key transformation failed");
        }
    }

    /**
     * Returns the private EC key of the specified EC key.
     *
     * @param key the EC key bytes. Must not be {@code null}.
     * @return the EC private key.
     * @throws CryptoException the EC key transformation failed.
     */
    public static ECPrivateKey toECPrivateKey(final byte[] key) throws CryptoException {
        final Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider();
        try {
            PKCS8EncodedKeySpec eks = new PKCS8EncodedKeySpec(key);
            KeyFactory keyFactory = KeyFactory.getInstance("EC", provider);
            ECPrivateKey privateKey = (BCECPrivateKey) keyFactory.generatePrivate(eks);
            if (privateKey == null) {
                throw new InvalidKeyException("The EC private key transformation failed");
            }
            return privateKey;
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            throw new InvalidKeyException("The EC private key transformation failed");
        }
    }

    /**
     * Returns the private EC key of the specified EC key.
     *
     * @param d the EC private key bytes. Must not be {@code null}.
     * @return the EC private key.
     * @throws CryptoException the EC key transformation failed.
     */
    public static ECPrivateKey convertECPrivateKey(final byte[] d) throws CryptoException {
        final Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider();
        try {
            X9ECParameters x9ECParameters = GMNamedCurves.getByName(SM2_CURVE_NAME);
            ECCurve curve = x9ECParameters.getCurve();
            ECNamedCurveSpec namedCurveSpec = new ECNamedCurveSpec(SM2_CURVE_NAME, curve, x9ECParameters.getG(), x9ECParameters.getN());

            KeyFactory keyFactory = KeyFactory.getInstance("EC", provider);
            ECPrivateKey privateKey = (BCECPrivateKey) keyFactory.generatePrivate(new ECPrivateKeySpec(BigIntegers.fromUnsignedByteArray(d), namedCurveSpec));
            if (privateKey == null) {
                throw new InvalidKeyException("The EC private key transformation failed");
            }
            return privateKey;
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            throw new InvalidKeyException("The EC private key transformation failed");
        }
    }

    /**
     * PublicKey convert to {@link ECPublicKeyParameters}.
     * 
     * @param ecPubKey the publicKey
     * @return the {@link ECPublicKeyParameters}
     */
    public static ECPublicKeyParameters convertPublicKeyToParameters(BCECPublicKey ecPubKey) {
        ECParameterSpec parameterSpec = ecPubKey.getParameters();
        ECDomainParameters domainParameters = new ECDomainParameters(parameterSpec.getCurve(), parameterSpec.getG(), parameterSpec.getN(), parameterSpec.getH());
        return new ECPublicKeyParameters(ecPubKey.getQ(), domainParameters);
    }

    /**
     * PrivateKey convert to {@link ECPrivateKeyParameters}.
     * 
     * @param ecPriKey the privateKey
     * @return the {@link ECPrivateKeyParameters}
     */
    public static ECPrivateKeyParameters convertPrivateKeyToParameters(BCECPrivateKey ecPriKey) {
        ECParameterSpec parameterSpec = ecPriKey.getParameters();
        ECDomainParameters domainParameters = new ECDomainParameters(parameterSpec.getCurve(), parameterSpec.getG(), parameterSpec.getN(), parameterSpec.getH());
        return new ECPrivateKeyParameters(ecPriKey.getD(), domainParameters);
    }

    /**
     * 根据私钥参数获取公钥参数.
     *
     * @param privateKeyParameters the privateKeyParameters
     * @return the {@link ECPublicKeyParameters}
     */
    public static ECPublicKeyParameters getPublicParams(ECPrivateKeyParameters privateKeyParameters) {
        final ECDomainParameters domainParameters = privateKeyParameters.getParameters();
        final org.bouncycastle.math.ec.ECPoint q = new FixedPointCombMultiplier().multiply(domainParameters.getG(), privateKeyParameters.getD());
        return new ECPublicKeyParameters(q, domainParameters);
    }

    /**
     * 私钥转换为 {@link ECPrivateKeyParameters}.
     *
     * @param privateKey the privateKey
     * @return the {@link ECPrivateKeyParameters}
     */
    public static ECPrivateKeyParameters toPrivateParams(PrivateKey privateKey) {
        if (null == privateKey) {
            return null;
        }
        try {
            return (ECPrivateKeyParameters) ECUtil.generatePrivateKeyParameter(privateKey);
        } catch (InvalidKeyException | java.security.InvalidKeyException e) {
            throw new CryptoException(e);
        }
    }

    /**
     * Returns the length in bits of the specified EC public key.
     *
     * @param publicKey The EC public key. Must not be {@code null}.
     * @return The key length in bits.
     */
    public static int keyBitLength(final PublicKey publicKey) {
        if (!(publicKey instanceof ECPublicKey)) {
            return -1;
        }

        ECPublicKey ecPublicKey = (ECPublicKey) publicKey;
        try {
            return ecPublicKey.getParams().getOrder().bitLength();
        } catch (Exception e) {
            return -1;
        }
    }

    /**
     * Returns the length in bits of the specified EC private key.
     *
     * @param privateKey The EC private key. Must not be {@code null}.
     * @return The key length in bits.
     */
    public static int keyBitLength(final PrivateKey privateKey) {
        if (!(privateKey instanceof ECPrivateKey)) {
            return -1;
        }

        ECPrivateKey ecPrivateKey = (ECPrivateKey) privateKey;
        try {
            return ecPrivateKey.getParams().getOrder().bitLength();
        } catch (Exception e) {
            return -1;
        }
    }
    
    /**
     * Check algorithm is SM2.
     * 
     * @param oid the oid
     * @return {@code true} if the algorithm is SM2
     */
    public static boolean isSM2Algorithm(ASN1ObjectIdentifier oid) {
        return oid.equals(GMObjectIdentifiers.sm2p256v1) || oid.equals(GMObjectIdentifiers.sm2sign) || oid.equals(GMObjectIdentifiers.sm2exchange)
                || oid.equals(GMObjectIdentifiers.sm2encrypt) || oid.equals(GMObjectIdentifiers.sm2sign_with_sm3) || oid.equals(X9ObjectIdentifiers.id_ecPublicKey);
    }
}
